package org.grayrabbit.cms.plugin.loaders;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.grayrabbit.cms.plugin.loaders.classloading.NonLockingJarHandler;
import org.grayrabbit.cms.utils.FileUtils;


public class PluginClassLoader extends ClassLoader{

	private static final String PLUGIN_INNER_JAR_PREFIX = "cms-plugins-innerjar";
	  private final List<File> pluginInnerJars;
	  private final Map<String,URL> entryMappings = new HashMap<String,URL>();

	  public PluginClassLoader(File pluginFile)
	  {
	    this(pluginFile, null);
	  }

	  public PluginClassLoader(File pluginFile, ClassLoader parent)
	  {
	    super(parent);
	    try
	    {
	      if ((pluginFile == null) || (!pluginFile.exists()))
	      {
	        throw new IllegalArgumentException("Plugin jar file must not be null and must exist.");
	      }
	      this.pluginInnerJars = new ArrayList<File>();
	      initializeFromJar(pluginFile, true);
	    }
	    catch (IOException e)
	    {
	      throw new IllegalStateException(e.getMessage());
	    }
	  }

	  private void initializeFromJar(File pluginFile, boolean isOuterJar)
	    throws IOException
	  {
	    JarFile jarFile = new JarFile(pluginFile);
	    try
	    {
	      for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements(); )
	      {
	        JarEntry jarEntry =entries.nextElement();
	        if ((isOuterJar) && (jarEntry.getName().startsWith("META-INF/lib/")) && (jarEntry.getName().endsWith(".jar")))
	          initialiseInnerJar(jarFile, jarEntry);
	        else
	          addEntryMapping(jarEntry, pluginFile, isOuterJar);
	      }
	    }
	    finally
	    {
	      jarFile.close();
	    }
	  }

	  private void initialiseInnerJar(JarFile jarFile, JarEntry jarEntry) throws IOException
	  {
	    File innerJarTmpFile = File.createTempFile(PLUGIN_INNER_JAR_PREFIX, ".jar");
	    InputStream inputStream = jarFile.getInputStream(jarEntry);
	    FileOutputStream fileOutputStream = new FileOutputStream(innerJarTmpFile);
	    FileUtils.copy(inputStream, fileOutputStream);
	    initializeFromJar(innerJarTmpFile, false);
	    this.pluginInnerJars.add(innerJarTmpFile);
	    inputStream.close();
	    fileOutputStream.close();
	  }

	  protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException
	  {
	    Class<?> c = findLoadedClass(name);
	    if (c != null) {
	      return c;
	    }
	    try
	    {
	      String path = name.replace('.', '/').concat(".class");
	      if (isEntryInPlugin(path))
	      {
	        try
	        {
	          return loadClassFromPlugin(name, path);
	        }
	        catch (IOException e)
	        {
	          throw new ClassNotFoundException(name, e);
	        }
	      }
	      return findClass(name);
	    }
	    catch (ClassNotFoundException ex)
	    {
	    }
	    return super.loadClass(name, resolve);
	  }

	  public URL getResource(String name)
	  {
	    if (isEntryInPlugin(name))
	    {
	      return this.entryMappings.get(name);
	    }

	    return super.getResource(name);
	  }

	  public void close()
	  {
	    for (File file : this.pluginInnerJars )
	    {
	      try
	      {
	        file.delete();
	      } catch (Exception ignored) {
	      }
	    }
	  }

	  List<File> getPluginInnerJars() {
	    return new ArrayList<File>(this.pluginInnerJars);
	  }

	  private void initializePackage(String name) throws IOException
	  {
	    int i = name.lastIndexOf('.');
	    if (i != -1)
	    {
	      String pkgname = name.substring(0, i);

	      Package pkg = getPackage(pkgname);
	      if (pkg == null)
	      {
	        definePackage(pkgname, null, null, null, null, null, null, null);
	      }
	    }
	  }

	  private Class<?> loadClassFromPlugin(String className, String path) throws IOException
	  {
	    URL resourceURL = this.entryMappings.get(path);
	    InputStream inputStream = resourceURL.openStream();
	    byte[] bytez = toByteArray(inputStream);
	    FileUtils.shutdownStream(inputStream);
	    initializePackage(className);
	    return defineClass(className, bytez, 0, bytez.length);
	  }

	  private byte[] toByteArray(InputStream input)
	    throws IOException
	  {
	    ByteArrayOutputStream output = new ByteArrayOutputStream();
	    FileUtils.copy(input, output);
	    return output.toByteArray();
	  }

	  private URL getUrlOfResourceInJar(String name, File jarFile)
	  {
	    try
	    {
	      URL url = new URL(new URL("jar:file:" + jarFile.getAbsolutePath() + "!/"), name, new NonLockingJarHandler());
	      return url;
	    }
	    catch (MalformedURLException e) {
	    	throw new RuntimeException(e);
	    }
	  }

	  private boolean isEntryInPlugin(String name)
	  {
	    return this.entryMappings.containsKey(name);
	  }

	  private void addEntryMapping(JarEntry jarEntry, File jarFile, boolean overrideExistingEntries)
	  {
	    if (overrideExistingEntries)
	    {
	      addEntryUrl(jarEntry, jarFile);
	    }
	    else if (!this.entryMappings.containsKey(jarEntry.getName()))
	    {
	      addEntryUrl(jarEntry, jarFile);
	    }
	  }

	  private void addEntryUrl(JarEntry jarEntry, File jarFile)
	  {
	    this.entryMappings.put(jarEntry.getName(), getUrlOfResourceInJar(jarEntry.getName(), jarFile));
	  }
}
